package org.spring.json;


import java.io.IOException;
import java.io.Reader;
import java.util.HashMap;

/**
 * @author chengzhengzheng
 * @date 2019/10/24
 **/
public class JsonReader {
	final TokenReader reader;
	final JsonObjectFactory jsonObjectFactory;

	public JsonReader(Reader reader, JsonObjectFactory jsonObjectFactory) {
		this.reader = new TokenReader(new CharReader(reader));
		this.jsonObjectFactory = jsonObjectFactory != null ? jsonObjectFactory
				: () -> {
			return new HashMap<String, Object>();
		};
	}

	boolean hasStatus(int expectedStatus) {
		return ((status & expectedStatus) > 0);
	}

	Stack stack;
	int status;

	public Object parse() throws IOException {
		stack = new Stack();
		status = STATUS_EXPECT_SINGLE_VALUE | STATUS_EXPECT_BEGIN_OBJECT | STATUS_EXPECT_BEGIN_ARRAY;
		for (; ; ) {
			Token currentToken = reader.readNextToken();
			switch (currentToken) {
				case STRING:
					if (hasStatus(STATUS_EXPECT_SINGLE_VALUE)) {
						// single string:
						String str = reader.readString();
						stack.push(StackValue.newJsonSingle(str));
						status = STATUS_EXPECT_END_DOCUMENT;
						continue;
					}
					if (hasStatus(STATUS_EXPECT_OBJECT_KEY)) {
						String str = reader.readString();
						stack.push(StackValue.newJsonObjectKey(str));
						status = STATUS_EXPECT_COLON;
						continue;
					}
					if (hasStatus(STATUS_EXPECT_OBJECT_VALUE)) {
						String str = reader.readString();
						String key = stack.pop(StackValue.TYPE_OBJECT_KEY).valueAsKey();
						stack.peek(StackValue.TYPE_OBJECT).valueAsObject().put(key, str);
						status = STATUS_EXPECT_COMMA | STATUS_EXPECT_END_OBJECT;
						continue;
					}
					if (hasStatus(STATUS_EXPECT_ARRAY_VALUE)) {
						String str = reader.readString();
						stack.peek(StackValue.TYPE_ARRAY).valueAsArray().add(str);
						status = STATUS_EXPECT_COMMA | STATUS_EXPECT_END_ARRAY;
						continue;
					}
					throw new JsonParseException("Unexpected char \'\"\'.", reader.reader.readed);

				case SEP_COLON: // :
					if (status == STATUS_EXPECT_COLON) {
						status = STATUS_EXPECT_OBJECT_VALUE | STATUS_EXPECT_BEGIN_OBJECT | STATUS_EXPECT_BEGIN_ARRAY;
						continue;
					}
					throw new JsonParseException("Unexpected char \':\'.", reader.reader.readed);

				case SEP_COMMA: // ,
					if (hasStatus(STATUS_EXPECT_COMMA)) {
						if (hasStatus(STATUS_EXPECT_END_OBJECT)) {
							status = STATUS_EXPECT_OBJECT_KEY;
							continue;
						}
						if (hasStatus(STATUS_EXPECT_END_ARRAY)) {
							status = STATUS_EXPECT_ARRAY_VALUE | STATUS_EXPECT_BEGIN_ARRAY | STATUS_EXPECT_BEGIN_OBJECT;
							continue;
						}
					}
					throw new JsonParseException("Unexpected char \',\'.", reader.reader.readed);
				case END_OBJECT:
					if (hasStatus(STATUS_EXPECT_END_OBJECT)) {
						StackValue object = stack.pop(StackValue.TYPE_OBJECT);
						if (stack.isEmpty()) {
							// root object:
							stack.push(object);
							status = STATUS_EXPECT_END_DOCUMENT;
							continue;
						}
						int type = stack.getTopValueType();
						if (type == StackValue.TYPE_OBJECT_KEY) {
							String key = stack.pop(StackValue.TYPE_OBJECT_KEY).valueAsKey();
							stack.peek(StackValue.TYPE_OBJECT).valueAsObject().put(key, object.value);
							status = STATUS_EXPECT_COMMA | STATUS_EXPECT_END_OBJECT;
							continue;
						}
						if (type == StackValue.TYPE_ARRAY) {
							stack.peek(StackValue.TYPE_ARRAY).valueAsArray().add(object.value);
							status = STATUS_EXPECT_COMMA | STATUS_EXPECT_END_ARRAY;
							continue;
						}
					}
					throw new JsonParseException("Unexpected char: \'}\'.", reader.reader.readed);

				case END_DOCUMENT:
					if (hasStatus(STATUS_EXPECT_END_DOCUMENT)) {
						StackValue v = stack.pop();
						if (stack.isEmpty()) {
							return v.value;
						}
					}
					throw new JsonParseException("Unexpected EOF.", reader.reader.readed);
				case BEGIN_OBJECT:
					if (hasStatus(STATUS_EXPECT_BEGIN_OBJECT)) {
						stack.push(StackValue.newJsonObject(this.jsonObjectFactory.createJsonObject()));
						status = STATUS_EXPECT_OBJECT_KEY | STATUS_EXPECT_BEGIN_OBJECT | STATUS_EXPECT_END_OBJECT;
						continue;
					}
					throw new JsonParseException("Unexpected char: \'{\'.", reader.reader.readed);
			}
		}
	}


	/**
	 * Should read EOF for next token.
	 */
	static final int STATUS_EXPECT_END_DOCUMENT = 0x0002;

	/**
	 * Should read "{" for next token.
	 */
	static final int STATUS_EXPECT_BEGIN_OBJECT = 0x0004;

	/**
	 * Should read "}" for next token.
	 */
	static final int STATUS_EXPECT_END_OBJECT = 0x0008;

	/**
	 * Should read object key for next token.
	 */
	static final int STATUS_EXPECT_OBJECT_KEY = 0x0010;

	/**
	 * Should read object value for next token.
	 */
	static final int STATUS_EXPECT_OBJECT_VALUE = 0x0020;

	/**
	 * Should read ":" for next token.
	 */
	static final int STATUS_EXPECT_COLON = 0x0040;

	/**
	 * Should read "," for next token.
	 */
	static final int STATUS_EXPECT_COMMA = 0x0080;

	/**
	 * Should read "[" for next token.
	 */
	static final int STATUS_EXPECT_BEGIN_ARRAY = 0x0100;

	/**
	 * Should read "]" for next token.
	 */
	static final int STATUS_EXPECT_END_ARRAY = 0x0200;

	/**
	 * Should read array value for next token.
	 */
	static final int STATUS_EXPECT_ARRAY_VALUE = 0x0400;

	/**
	 * Should read a single value for next token (must not be "{" or "[").
	 */
	static final int STATUS_EXPECT_SINGLE_VALUE = 0x0800;
}


class TokenReader {
	private String source;
	CharReader reader;

	TokenReader(CharReader reader) {
		this.reader = reader;
	}

	boolean isWhiteSpace(char ch) {
		return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r';
	}

	String readString() throws IOException {
		StringBuilder sb = new StringBuilder(50);
		// first char must be ":
		char ch = reader.next();
		if (ch != '\"') {
			throw new JsonParseException("Expected \" but actual is: " + ch,
					reader.readed);
		}
		for (; ; ) {
			ch = reader.next();
			if (ch == '\\') {
				// escape: \" \\ \/ \b \f \n \r \t
				char ech = reader.next();
				switch (ech) {
					case '\"':
						sb.append('\"');
						break;
					case '\\':
						sb.append('\\');
						break;
					case '/':
						sb.append('/');
						break;
					case 'b':
						sb.append('\b');
						break;
					case 'f':
						sb.append('\f');
						break;
					case 'n':
						sb.append('\n');
						break;
					case 'r':
						sb.append('\r');
						break;
					case 't':
						sb.append('\t');
						break;
					case 'u':
						// read an unicode uXXXX:
						int u = 0;
						for (int i = 0; i < 4; i++) {
							char uch = reader.next();
							if (uch >= '0' && uch <= '9') {
								u = (u << 4) + (uch - '0');
							} else if (uch >= 'a' && uch <= 'f') {
								u = (u << 4) + (uch - 'a') + 10;
							} else if (uch >= 'A' && uch <= 'F') {
								u = (u << 4) + (uch - 'A') + 10;
							} else {
								throw new JsonParseException("Unexpected char: " + uch,
										reader.readed);
							}
						}
						sb.append((char) u);
						break;
					default:
						throw new JsonParseException("Unexpected char: " + ch,
								reader.readed);
				}
			} else if (ch == '\"') {
				// end of string:
				break;
			} else if (ch == '\r' || ch == '\n') {
				throw new JsonParseException("Unexpected char: " + ch,
						reader.readed);
			} else {
				sb.append(ch);
			}
		}
		return sb.toString();
	}

	Token readNextToken() throws IOException {
		char ch = '?';
		for (; ; ) {
			if (!reader.hasMore()) {
				// EOF:
				return Token.END_DOCUMENT;
			}
			ch = reader.peek();
			if (!isWhiteSpace(ch)) {
				break;
			}
			reader.next(); // skip white space
		}
		switch (ch) {
			case '{':
				reader.next(); // skip
				return Token.BEGIN_OBJECT;
			case '}':
				reader.next(); // skip
				return Token.END_OBJECT;
			case '[':
				reader.next(); // skip
				return Token.BEGIN_ARRAY;
			case ']':
				reader.next(); // skip
				return Token.END_ARRAY;
			case ':':
				reader.next(); // skip
				return Token.SEP_COLON;
			case ',':
				reader.next(); // skip
				return Token.SEP_COMMA;
			case '\"':
				return Token.STRING;
			case 'n':
				return Token.NULL;
			// true / false
			case 't':
			case 'f':
				return Token.BOOLEAN;
			case '-':
				return Token.NUMBER;
		}
		if (ch >= '0' && ch <= '9') {
			return Token.NUMBER;
		}
//		throw new JsonParseException("Parse error when try to guess next token.",
//				reader.readed);
		throw new RuntimeException("");
	}
}

class CharReader {

	static final int BUFFER_SIZE = 1024;

	// total readed chars:
	int readed = 0;

	// buffer position:
	int pos = 0;

	// buffer ends:
	int size = 0;

	final char[] buffer;
	final Reader reader;

	public CharReader(Reader reader) {
		this.buffer = new char[BUFFER_SIZE];
		this.reader = reader;
	}

	public boolean hasMore() throws IOException {
		if (pos < size) {
			return true;
		}
		// try fill:
		fillBuffer(null);
		return pos < size;
	}

	public String next(int size) throws IOException {
		StringBuilder sb = new StringBuilder(size);
		for (int i = 0; i < size; i++) {
			sb.append(next());
		}
		return sb.toString();
	}

	public char next() throws IOException {
		if (this.pos == this.size) {
			// fill buffer:
			fillBuffer("EOF");
		}
		char ch = this.buffer[this.pos];
		this.pos++;
		return ch;
	}

	public char peek() throws IOException {
		if (this.pos == this.size) {
			// fill buffer:
			fillBuffer("EOF");
		}
		assert (this.pos < this.size);
		return this.buffer[this.pos];
	}

	void fillBuffer(String eofErrorMessage) throws IOException {
		int n = reader.read(buffer);
		if (n == (-1)) {
			if (eofErrorMessage != null) {
//				throw new JsonParseException(eofErrorMessage, this.readed);
			}
			return;
		}
		this.pos = 0;
		this.size = n;
		this.readed += n;
	}
}
